<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Faster R-CNN 4步训练演示 (含输入流)</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f0f2f5;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
        }
        h1 { color: #333; }
        .container {
            position: relative;
            width: 900px;
            height: 600px;
            background-color: white;
            border-radius: 15px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.1);
            overflow: hidden;
            border: 1px solid #ddd;
        }
        canvas { display: block; }
        .controls {
            margin-top: 20px;
            display: flex;
            gap: 20px;
            align-items: center;
        }
        button {
            padding: 10px 25px;
            font-size: 16px;
            cursor: pointer;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            transition: background 0.2s;
        }
        button:hover { background-color: #0056b3; }
        button:disabled { background-color: #ccc; cursor: not-allowed; }

        .description-box {
            margin-top: 20px;
            width: 900px;
            background: #fff;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.05);
            min-height: 100px;
        }
        .step-title { font-size: 18px; font-weight: bold; color: #007bff; margin-bottom: 10px; }
        .step-desc { font-size: 16px; color: #555; line-height: 1.6; }
        .highlight { color: #e74c3c; font-weight: bold; }
        .input-highlight { color: #e67e22; font-weight: bold; }

        /* Legends */
        .legend { margin-top: 10px; display: flex; gap: 15px; font-size: 14px; color: #666; }
        .dot { width: 12px; height: 12px; display: inline-block; border-radius: 50%; margin-right: 5px; }
    </style>
</head>
<body>

    <h1>Faster R-CNN 4步训练演示 (含数据流)</h1>

    <div class="container">
        <canvas id="animCanvas" width="900" height="600"></canvas>
    </div>

    <div class="controls">
        <button id="prevBtn" onclick="prevStep()">上一步</button>
        <span id="stepIndicator" style="font-weight: bold; font-size: 18px;">Step 1 / 4</span>
        <button id="nextBtn" onclick="nextStep()">下一步</button>
    </div>

    <div class="legend">
        <span><span class="dot" style="background:#4CAF50"></span>正在训练</span>
        <span><span class="dot" style="background:#ddd; border:1px solid #999"></span>冻结 (Fixed)</span>
        <span><span class="dot" style="background:#f39c12"></span>输入数据</span>
        <span><span class="dot" style="background:#9b59b6"></span>中间数据 (Proposals)</span>
    </div>

    <div class="description-box">
        <div id="descTitle" class="step-title">第一步：训练 RPN</div>
        <div id="descText" class="step-desc"></div>
    </div>

    <script>
        const canvas = document.getElementById('animCanvas');
        const ctx = canvas.getContext('2d');
        let currentStep = 1;
        let particles = [];

        // 状态定义
        const steps = [
            { // Step 1
                title: "第一步：RPN 独自练级",
                desc: "输入：<span class='input-highlight'>整张图片 (Image)</span>。<br>流向：图片 -> 共享层 -> RPN -> <span class='highlight'>输出建议框 P1</span>。<br>状态：RPN 和 共享层都在训练。",
                shared: 1, rpn: 1, det: 0,
                inputProp: 'output', // Proposals are output here
                propName: 'Proposals P1',
                initText: "初始化：ImageNet"
            },
            { // Step 2
                title: "第二步：检测器 (Fast R-CNN) 独自练级",
                desc: "输入：<span class='input-highlight'>图片</span> + <span class='input-highlight'>Step 1 生成的建议框 P1 (固定)</span>。<br>流向：RPN 被拿走了。检测器接收图片特征和固定的建议框来训练。<br>状态：检测器和共享层都在训练 (新的权重)。",
                shared: 1, rpn: 0, det: 1,
                inputProp: 'input', // Proposals are input here
                propName: 'Fixed P1',
                initText: "初始化：ImageNet (新的)"
            },
            { // Step 3
                title: "第三步：RPN 回归 (开始共享)",
                desc: "输入：<span class='input-highlight'>图片</span>。<br>流向：图片 -> 共享层(锁住) -> RPN -> <span class='highlight'>输出建议框 P2</span>。<br>状态：利用 Step 2 的检测器权重来初始化。共享层冻结，只练 RPN。",
                shared: 2, rpn: 1, det: 2,
                inputProp: 'output',
                propName: 'Proposals P2',
                initText: "初始化：来自 Step 2 的权重"
            },
            { // Step 4
                title: "第四步：检测器回归 (最终合体)",
                desc: "输入：<span class='input-highlight'>图片</span> + <span class='input-highlight'>Step 3 生成的建议框 P2 (固定)</span>。<br>流向：图片 -> 共享层(锁住) -> 特征 + 建议框 -> 检测器。<br>状态：共享层冻结，RPN 冻结，只练检测器。",
                shared: 2, rpn: 2, det: 1,
                inputProp: 'input',
                propName: 'Fixed P2',
                initText: "初始化：来自 Step 3 的权重"
            }
        ];

        function createParticle(x, y, targetX, targetY, color) {
            return { x, y, tx: targetX, ty: targetY, speed: 2 + Math.random(), life: 100, color: color };
        }

        function drawArrow(fromX, fromY, toX, toY, color) {
            ctx.beginPath();
            ctx.moveTo(fromX, fromY);
            ctx.lineTo(toX, toY);
            ctx.strokeStyle = color;
            ctx.lineWidth = 2;
            ctx.stroke();
            // Arrow head
            let angle = Math.atan2(toY - fromY, toX - fromX);
            ctx.beginPath();
            ctx.moveTo(toX, toY);
            ctx.lineTo(toX - 10 * Math.cos(angle - Math.PI / 6), toY - 10 * Math.sin(angle - Math.PI / 6));
            ctx.lineTo(toX - 10 * Math.cos(angle + Math.PI / 6), toY - 10 * Math.sin(angle + Math.PI / 6));
            ctx.fillStyle = color;
            ctx.fill();
        }

        function drawBox(x, y, w, h, color, text, subtext, strokeColor="#999", isFixed=false) {
            // Draw box
            ctx.beginPath();
            ctx.roundRect(x, y, w, h, 10);
            ctx.fillStyle = color;
            ctx.fill();
            ctx.lineWidth = isFixed ? 1 : 3;
            ctx.strokeStyle = strokeColor;
            ctx.stroke();

            // Text
            ctx.fillStyle = "#333";
            ctx.font = "bold 16px Arial";
            ctx.textAlign = "center";
            ctx.fillText(text, x + w/2, y + h/2 - 5);
            if(subtext) {
                ctx.font = "12px Arial";
                ctx.fillStyle = "#666";
                ctx.fillText(subtext, x + w/2, y + h/2 + 15);
            }

            // Lock icon
            if(isFixed) {
                ctx.fillStyle = "#555";
                ctx.font = "20px Arial";
                ctx.fillText("🔒", x + w - 20, y + 30);
            }
        }

        function drawNet() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            const state = steps[currentStep - 1];

            // 颜色定义
            const c_train = "#E8F5E9"; // Green bg
            const c_train_border = "#4CAF50";
            const c_fixed = "#F2F3F4"; // Grey bg
            const c_fixed_border = "#BDC3C7";
            const c_input = "#FEF9E7"; // Yellow bg
            const c_input_border = "#F1C40F";
            const c_prop = "#F4ECF7"; // Purple bg
            const c_prop_border = "#9B59B6";

            // --- 1. Draw Image Input (Bottom) ---
            drawBox(350, 500, 200, 60, c_input, "Input Image", "Ground Truth", c_input_border);
            // Arrow to Base
            drawArrow(450, 500, 450, 400, "#aaa");
            // Particle: Image -> Base
            if(Math.random() > 0.8) particles.push(createParticle(450, 500, 450, 400, "#F1C40F"));


            // --- 2. Draw Shared Base ---
            let baseColor = state.shared === 1 ? c_train : c_fixed;
            let baseBorder = state.shared === 1 ? c_train_border : c_fixed_border;
            drawBox(250, 300, 400, 100, baseColor, "Shared Conv Layers", "VGG / ZF", baseBorder, state.shared === 2);


            // --- 3. RPN Path (Left) ---
            if(state.rpn !== 0) {
                let rpnColor = state.rpn === 1 ? "#FDEDEC" : c_fixed;
                let rpnBorder = state.rpn === 1 ? "#E74C3C" : c_fixed_border;

                // Connection
                drawArrow(350, 300, 350, 200, "#aaa");

                drawBox(250, 100, 200, 100, rpnColor, "RPN Head", "3x3 Conv + 1x1 Convs", rpnBorder, state.rpn === 2);

                // Proposals Output logic (Step 1 & 3)
                if(state.inputProp === 'output') {
                    // Arrow OUT
                    drawArrow(450, 150, 600, 150, "#9B59B6");
                    drawBox(600, 120, 150, 60, c_prop, "Output:", state.propName, c_prop_border);

                    // Particle: Base -> RPN
                    if(state.rpn === 1 && Math.random() > 0.8) particles.push(createParticle(350, 300, 350, 200, "#E74C3C"));
                    // Particle: RPN -> Output
                    if(state.rpn === 1 && Math.random() > 0.8) particles.push(createParticle(450, 150, 600, 150, "#9B59B6"));
                }
            } else {
                // Ghost RPN
                ctx.strokeStyle = "#eee"; ctx.setLineDash([5, 5]); ctx.strokeRect(250, 100, 200, 100); ctx.setLineDash([]);
                ctx.fillStyle = "#ccc"; ctx.fillText("RPN (Not Active)", 350, 150);
            }


            // --- 4. Detector Path (Right) ---
            if(state.det !== 0) {
                let detColor = state.det === 1 ? "#EAF2F8" : c_fixed;
                let detBorder = state.det === 1 ? "#3498DB" : c_fixed_border;

                // Connection Base -> Detector
                drawArrow(550, 300, 550, 200, "#aaa");

                drawBox(450, 100, 200, 100, detColor, "Detector Head", "RoI Pooling + FC", detBorder, state.det === 2);

                // Proposals Input logic (Step 2 & 4)
                if(state.inputProp === 'input') {
                    // Draw Input Box on right
                    drawBox(700, 250, 150, 60, c_prop, "Input:", state.propName, c_prop_border);

                    // Arrow INTO Detector
                    drawArrow(775, 250, 650, 150, "#9B59B6");

                    // Particle: Prop Input -> Detector
                    if(state.det === 1 && Math.random() > 0.8) particles.push(createParticle(775, 250, 650, 150, "#9B59B6"));
                    // Particle: Base -> Detector
                    if(state.det === 1 && Math.random() > 0.8) particles.push(createParticle(550, 300, 550, 200, "#3498DB"));
                }
            } else {
                // Ghost Detector
                ctx.strokeStyle = "#eee"; ctx.setLineDash([5, 5]); ctx.strokeRect(450, 100, 200, 100); ctx.setLineDash([]);
                ctx.fillStyle = "#ccc"; ctx.fillText("Detector (Not Active)", 550, 150);
            }

            // Init Text
            ctx.fillStyle = "#666";
            ctx.font = "italic 14px Arial";
            ctx.textAlign = "left";
            ctx.fillText(state.initText, 50, 550);

            updateParticles();
            requestAnimationFrame(drawNet);
        }

        function updateParticles() {
            for (let i = 0; i < particles.length; i++) {
                let p = particles[i];
                ctx.beginPath();
                ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
                ctx.fillStyle = p.color;
                ctx.fill();

                // Simple move towards target
                let dx = p.tx - p.x;
                let dy = p.ty - p.y;
                let dist = Math.sqrt(dx*dx + dy*dy);

                if(dist < p.speed) {
                    p.life = 0; // Arrived
                } else {
                    p.x += (dx/dist) * p.speed;
                    p.y += (dy/dist) * p.speed;
                }

                p.life--;
                if (p.life <= 0) {
                    particles.splice(i, 1);
                    i--;
                }
            }
        }

        function updateUI() {
            const state = steps[currentStep - 1];
            document.getElementById('descTitle').innerText = state.title;
            document.getElementById('descText').innerHTML = state.desc;
            document.getElementById('stepIndicator').innerText = `Step ${currentStep} / 4`;

            document.getElementById('prevBtn').disabled = currentStep === 1;
            document.getElementById('nextBtn').disabled = currentStep === 4;

            particles = []; // Clear particles on step change
        }

        function nextStep() {
            if (currentStep < 4) {
                currentStep++;
                updateUI();
            }
        }

        function prevStep() {
            if (currentStep > 1) {
                currentStep--;
                updateUI();
            }
        }

        // Start
        updateUI();
        drawNet();

    </script>
</body>
</html>